package com.moc.common.utils.minio;

import com.moc.common.core.domain.entity.SysMinioFile;
import com.moc.common.exception.file.FileSizeLimitExceededException;
import com.moc.common.exception.file.InvalidExtensionException;
import com.moc.common.utils.DateUtils;
import com.moc.common.utils.SecurityUtils;
import com.moc.common.utils.file.MimeTypeUtils;
import io.minio.*;
import io.minio.errors.ErrorResponseException;
import io.minio.http.Method;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * MinIO上传工具类
 * 支持：普通上传、分片上传（含断点续传/MD5校验/前端直传）、下载、删除、预签名URL
 */
@Slf4j
@Component
public class MinioUploadUtil {

    /** 普通上传默认大小限制：50M */
    public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L;
    /** 分片上传默认大小：5MB（MinIO最小分片限制） */
    public static final long DEFAULT_PART_SIZE = 5 * 1024 * 1024L;
    /** 分片临时存储目录：temp/parts/{uploadId}/ */
    private static final String TEMP_PART_DIR = "temp/parts/";
    /** 分片预签名URL有效期：1小时 */
    private static final int PART_PRESIGNED_URL_EXPIRY = 1;

    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.bucketName}")
    private String defaultBucket;
    @Autowired
    private MinioClient minioClient;

    /**
     * 验证MinIO连接状态（所有操作前触发）
     */
    public Boolean validateMinioConnection() {
        try {
            minioClient.listBuckets();
            log.info("MinIO连接验证成功");
        } catch (Exception e) {
            log.error("MinIO连接失败：{}", e.getMessage(), e);
            throw new RuntimeException("MinIO服务连接失败，请检查配置或服务状态");
        }
        return true;
    }

    // ------------------------------ 普通上传（保持原有逻辑） ------------------------------
    public SysMinioFile upload(MultipartFile file) {
        validateMinioConnection();
        return upload(file, null, null);
    }

    public SysMinioFile upload(MultipartFile file, String fileName) {
        validateMinioConnection();
        return upload(file, fileName, null);
    }

    public SysMinioFile upload(MultipartFile file, String fileName, String customDir) {
        validateMinioConnection();
        try {
            validateFile(file);

            String fileExt = getFileExtension(file.getOriginalFilename());
            String finalFileName = (fileName != null ? fileName : UUID.randomUUID().toString()) + "." + fileExt;
            String dir = customDir != null ? ensureDirFormat(customDir) : DateUtils.dateTimeNow("YYYYMMDD") + "/";
            String objectName = dir + finalFileName;

            // 执行普通上传
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(objectName)
                            .stream(file.getInputStream(), file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build()
            );

            // 构建返回结果
            return buildSysMinioFile(finalFileName, objectName, file, calculateMD5(file));
        } catch (Exception e) {
            log.error("普通上传失败：{}", e.getMessage(), e);
            throw new RuntimeException("文件上传失败：" + e.getMessage());
        }
    }

    // ------------------------------ 分片上传核心功能（完善版） ------------------------------

    /**
     * 1. 初始化分片上传（生成uploadId，支持自定义分片大小）
     * @param originalFilename 原始文件名（如"test.zip"）
     * @param contentType 文件MIME类型（如"application/zip"）
     * @param customPartSize 自定义分片大小（null则用默认5MB）
     * @return 初始化结果：uploadId + 分片大小 + 预期总分片数（示例：{"uploadId":"xxx","partSize":5242880,"expectedPartCount":10}）
     */
    public Map<String, Object> initMultipartUpload(String originalFilename, String contentType, Long customPartSize, long fileTotalSize) {
        validateMinioConnection();
        try {
            // 生成唯一uploadId（标识本次分片任务）
            String uploadId = UUID.randomUUID().toString().replace("-", "");
            // 确定分片大小（优先自定义，否则用默认）
            long partSize = customPartSize != null && customPartSize >= DEFAULT_PART_SIZE
                    ? customPartSize
                    : DEFAULT_PART_SIZE;
            // 计算预期总分片数
            int expectedPartCount = calculatePartCount(fileTotalSize, partSize);

            // 构建返回结果
            Map<String, Object> result = new HashMap<>(4);
            result.put("uploadId", uploadId);
            result.put("partSize", partSize);
            result.put("expectedPartCount", expectedPartCount);
            result.put("originalFilename", originalFilename);
            result.put("contentType", contentType);
            log.info("分片上传初始化成功：uploadId={}, 分片大小={}MB, 预期分片数={}",
                    uploadId, partSize / 1024 / 1024, expectedPartCount);
            return result;
        } catch (Exception e) {
            log.error("分片初始化失败：{}", e.getMessage(), e);
            throw new RuntimeException("分片上传初始化失败：" + e.getMessage());
        }
    }

    /**
     * 2. 生成分片预签名URL（前端直传用，避免服务端中转分片）
     * @param uploadId 分片任务ID
     * @param partNumber 分片序号（从1开始）
     * @return 分片直传URL + 分片临时路径
     */
    public Map<String, String> getPartPresignedUrl(String uploadId, int partNumber) {
        validateMinioConnection();
        try {
            String tempPartPath = getTempPartPath(uploadId, partNumber);
            // 生成PUT方式的预签名URL（前端直接用该URL上传分片）
            String presignedUrl = minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.PUT)
                            .bucket(defaultBucket)
                            .object(tempPartPath)
                            .expiry(PART_PRESIGNED_URL_EXPIRY, TimeUnit.HOURS)
                            .build()
            );

            Map<String, String> result = new HashMap<>(2);
            result.put("presignedUrl", presignedUrl);
            result.put("tempPartPath", tempPartPath);
            log.debug("分片{}预签名URL生成成功：uploadId={}, url={}", partNumber, uploadId, presignedUrl);
            return result;
        } catch (Exception e) {
            log.error("分片预签名URL生成失败：uploadId={}, partNumber={}", uploadId, partNumber, e);
            throw new RuntimeException("生成分片上传URL失败：" + e.getMessage());
        }
    }

    /**
     * 3. 服务端上传分片（适用于服务端接收分片后上传，非前端直传场景）
     * @param uploadId 分片任务ID
     * @param partNumber 分片序号
     * @param partFile 分片文件（MultipartFile）
     * @param partMd5 分片MD5（用于校验分片完整性）
     * @return 分片临时路径 + 分片大小
     */
    public Map<String, Object> uploadPartByServer(String uploadId, int partNumber, MultipartFile partFile, String partMd5) {
        validateMinioConnection();
        try {
            // 1. 校验分片MD5（避免传输损坏）
            String calculatedMd5 = calculateMD5(partFile);
            if (!calculatedMd5.equalsIgnoreCase(partMd5)) {
                throw new RuntimeException("分片" + partNumber + "MD5校验失败：上传MD5=" + partMd5 + ", 实际MD5=" + calculatedMd5);
            }

            // 2. 上传分片到临时路径
            String tempPartPath = getTempPartPath(uploadId, partNumber);
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(tempPartPath)
                            .stream(partFile.getInputStream(), partFile.getSize(), -1)
                            .build()
            );

            // 3. 返回结果
            Map<String, Object> result = new HashMap<>(3);
            result.put("tempPartPath", tempPartPath);
            result.put("partSize", partFile.getSize());
            result.put("partMd5", calculatedMd5);
            log.debug("服务端上传分片成功：uploadId={}, partNumber={}", uploadId, partNumber);
            return result;
        } catch (Exception e) {
            log.error("服务端上传分片失败：uploadId={}, partNumber={}", uploadId, partNumber, e);
            throw new RuntimeException("分片" + partNumber + "上传失败：" + e.getMessage());
        }
    }

    /**
     * 4. 查询已上传分片（断点续传核心：避免重复上传）
     * @param uploadId 分片任务ID
     * @return 已上传分片列表（key=分片序号，value=分片大小）
     */
    public Map<Integer, Long> listUploadedParts(String uploadId) {
        validateMinioConnection();
        try {
            String tempPartPrefix = TEMP_PART_DIR + uploadId + "/";
            Map<Integer, Long> uploadedParts = new TreeMap<>(); // TreeMap保证序号有序

            // 列举临时目录下的所有分片
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder()
                            .bucket(defaultBucket)
                            .prefix(tempPartPrefix)
                            .recursive(false) // 非递归（只查当前目录）
                            .build()
            );

            // 解析分片序号和大小
            for (Result<Item> result : results) {
                Item item = result.get();
                if (!item.isDir()) { // 排除目录本身
                    String objectName = item.objectName();
                    // 从路径中提取分片序号：temp/parts/{uploadId}/1 → 1
                    String partNumberStr = objectName.substring(objectName.lastIndexOf("/") + 1);
                    int partNumber = Integer.parseInt(partNumberStr);
                    uploadedParts.put(partNumber, item.size());
                }
            }

            log.debug("查询已上传分片完成：uploadId={}, 已上传{}个分片", uploadId, uploadedParts.size());
            return uploadedParts;
        } catch (Exception e) {
            log.error("查询已上传分片失败：uploadId={}", uploadId, e);
            throw new RuntimeException("查询已上传分片失败：" + e.getMessage());
        }
    }

    /**
     * 5. 完成分片上传（合并分片 + 清理临时文件）
     * @param uploadId 分片任务ID
     * @param partCount 总分片数（校验是否所有分片都已上传）
     * @param originalFilename 原始文件名
     * @param fileTotalSize 文件总大小（校验合并后完整性）
     * @param fileTotalMd5 整个文件的MD5（最终校验）
     * @param customDir 自定义存储目录（null则用默认日期目录）
     * @return 合并后的文件信息
     */
    public SysMinioFile completeMultipartUpload(String uploadId, int partCount, String originalFilename,
                                                long fileTotalSize, String fileTotalMd5, String customDir) {
        validateMinioConnection();
        try {
            // 1. 先校验已上传分片是否完整
            Map<Integer, Long> uploadedParts = listUploadedParts(uploadId);
            if (uploadedParts.size() != partCount) {
                throw new RuntimeException("分片数量不完整：已上传" + uploadedParts.size() + "个，需" + partCount + "个");
            }
            // 校验分片序号是否连续（从1到partCount）
            for (int i = 1; i <= partCount; i++) {
                if (!uploadedParts.containsKey(i)) {
                    throw new RuntimeException("分片" + i + "缺失，无法合并");
                }
            }

            // 2. 构建分片合并源（按序号顺序）
            List<ComposeSource> composeSources = new ArrayList<>();
            for (int i = 1; i <= partCount; i++) {
                composeSources.add(
                        ComposeSource.builder()
                                .bucket(defaultBucket)
                                .object(getTempPartPath(uploadId, i))
                                .build()
                );
            }

            // 3. 构建最终文件路径
            String fileExt = getFileExtension(originalFilename);
            String finalFileName = UUID.randomUUID().toString() + "." + fileExt;
            String dir = customDir != null ? ensureDirFormat(customDir) : DateUtils.dateTimeNow("YYYYMMDD") + "/";
            String finalObjectPath = dir + finalFileName;

            // 4. 执行分片合并
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(finalObjectPath)
                            .sources(composeSources)
                            .build()
            );

            // 5. 校验合并后文件的完整性（MD5 + 大小）
            StatObjectResponse statResp = minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(finalObjectPath)
                            .build()
            );
            if (statResp.size() != fileTotalSize) {
                throw new RuntimeException("文件合并后大小不匹配：预期" + fileTotalSize + "，实际" + statResp.size());
            }
            // 注：MinIO的statObject不直接返回MD5，需通过ETag校验（若上传时未指定加密，ETag≈MD5）
            if (statResp.etag() != null && !statResp.etag().replace("\"", "").equalsIgnoreCase(fileTotalMd5)) {
                log.warn("文件MD5校验警告：预期{}，ETag{}（若启用加密，此校验可能失效）", fileTotalMd5, statResp.etag());
            }

            // 6. 清理临时分片文件（合并成功后执行）
            cleanTempParts(uploadId, partCount);

            // 7. 构建返回结果
            SysMinioFile minioFile = new SysMinioFile();
            minioFile.setFileName(finalFileName);
            minioFile.setFileMd5(fileTotalMd5);
            minioFile.setBucketName(defaultBucket);
            minioFile.setFileExtension(fileExt);
            minioFile.setFileSize(fileTotalSize);
            minioFile.setFileType(getContentTypeByExt(fileExt)); // 补充文件类型
            minioFile.setStoragePath(finalObjectPath);
            minioFile.setOriginalFilename(originalFilename);
            minioFile.setUploadTime(DateUtils.getNowDate());
            minioFile.setUploadUserId(SecurityUtils.getUsername());

            log.info("分片合并成功：uploadId={}, 最终路径={}", uploadId, finalObjectPath);
            return minioFile;
        } catch (Exception e) {
            log.error("分片合并失败：uploadId={}", uploadId, e);
            throw new RuntimeException("分片合并失败：" + e.getMessage());
        }
    }

    /**
     * 6. 取消分片上传（清理临时分片，避免存储浪费）
     * @param uploadId 分片任务ID
     * @param partCount 已上传的分片数（若未知，传0则自动查询）
     */
    public void abortMultipartUpload(String uploadId, Integer partCount) {
        validateMinioConnection();
        try {
            // 若未指定已上传分片数，自动查询
            int actualPartCount = partCount != null && partCount > 0
                    ? partCount
                    : listUploadedParts(uploadId).size();

            if (actualPartCount > 0) {
                cleanTempParts(uploadId, actualPartCount);
                log.info("取消分片上传并清理临时文件：uploadId={}, 清理{}个分片", uploadId, actualPartCount);
            } else {
                log.info("取消分片上传：uploadId={}，无已上传分片", uploadId);
            }
        } catch (Exception e) {
            log.error("取消分片上传失败：uploadId={}", uploadId, e);
            throw new RuntimeException("取消分片上传失败：" + e.getMessage());
        }
    }

    // ------------------------------ 下载/删除/预签名URL（保持原有逻辑） ------------------------------
    public InputStream download(String objectName) {
        validateMinioConnection();
        try {
            return minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(objectName)
                            .build()
            );
        } catch (Exception e) {
            log.error("文件下载失败：{}", objectName, e);
            throw new RuntimeException("文件下载失败：" + e.getMessage());
        }
    }

    /**
     * 构建完整访问路径
     * @param objectName
     * @return
     */
    public String buildAccessPath(String objectName) {
        return endpoint + "/" + defaultBucket + "/" + objectName;
    }

    /**
     * 生成预签名URL 默认值为1小时
     * @param objectName
     * @return
     */
    public String getPresignedUrl(String objectName) {
        return getPresignedUrl(objectName, 1);
    }

    /**
     * 生成预签名URL
     * @param objectName
     * @param expiryHours
     * @return
     */
    public String getPresignedUrl(String objectName, int expiryHours) {
        validateMinioConnection();
        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .bucket(defaultBucket)
                            .object(objectName)
                            .method(Method.GET)
                            .expiry(expiryHours, TimeUnit.HOURS)
                            .build()
            );
        } catch (Exception e) {
            log.error("生成预签名URL失败：{}", objectName, e);
            throw new RuntimeException("生成预签名URL失败：" + e.getMessage());
        }
    }

    public boolean delete(String objectName) {
        validateMinioConnection();
        try {
            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(objectName)
                            .build()
            );
            log.info("文件删除成功：{}", objectName);
            return true;
        } catch (Exception e) {
            log.error("文件删除失败：{}", objectName, e);
            return false;
        }
    }

    // ------------------------------ 辅助工具方法 ------------------------------

    /**
     * 计算分片数量
     * @param fileTotalSize 文件总大小
     * @param partSize 分片大小
     * @return 总分片数（向上取整）
     */
    public int calculatePartCount(long fileTotalSize, long partSize) {
        if (fileTotalSize <= 0 || partSize <= 0) {
            return 1;
        }
        return (int) Math.ceil((double) fileTotalSize / partSize);
    }

    /**
     * 获取分片临时路径
     * @param uploadId 分片任务ID
     * @param partNumber 分片序号
     * @return 临时路径（如"temp/parts/123456/3"）
     */
    private String getTempPartPath(String uploadId, int partNumber) {
        return TEMP_PART_DIR + uploadId + "/" + partNumber;
    }

    /**
     * 清理临时分片文件
     */
    private void cleanTempParts(String uploadId, int partCount) throws Exception {
        for (int i = 1; i <= partCount; i++) {
            String tempPartPath = getTempPartPath(uploadId, i);
            // 忽略删除不存在的分片（避免异常）
            if (isObjectExists(tempPartPath)) {
                minioClient.removeObject(
                        RemoveObjectArgs.builder()
                                .bucket(defaultBucket)
                                .object(tempPartPath)
                                .build()
                );
                log.debug("清理临时分片：{}", tempPartPath);
            }
        }
        // 清理分片目录（若为空）
        String partDir = TEMP_PART_DIR + uploadId + "/";
        if (isObjectExists(partDir) && minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(defaultBucket)
                .prefix(partDir)
                .recursive(false)
                .build()).iterator().hasNext()) {
            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(partDir)
                            .build()
            );
            log.debug("清理分片目录：{}", partDir);
        }
    }

    /**
     * 检查MinIO中对象是否存在
     */
    private boolean isObjectExists(String objectName) {
        try {
            minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(defaultBucket)
                            .object(objectName)
                            .build()
            );
            return true;
        } catch (Exception e) {
            // 捕获"对象不存在"异常（MinIO的ErrorResponseException，statusCode=404）
            if (e instanceof ErrorResponseException && ((ErrorResponseException) e).errorResponse().code().equals("NoSuchKey")) {
                return false;
            }
            log.error("检查对象存在性失败：{}", objectName, e);
            throw new RuntimeException("检查文件存在性失败：" + e.getMessage());
        }
    }

    /**
     * 构建SysMinioFile（普通上传用）
     */
    private SysMinioFile buildSysMinioFile(String finalFileName, String objectName, MultipartFile file, String md5) {
        SysMinioFile minioFile = new SysMinioFile();
        minioFile.setFileName(finalFileName);
        minioFile.setFileMd5(md5);
        minioFile.setBucketName(defaultBucket);
        minioFile.setFileExtension(getFileExtension(file.getOriginalFilename()));
        minioFile.setFileSize(file.getSize());
        minioFile.setFileType(file.getContentType());
        minioFile.setStoragePath(objectName);
        minioFile.setOriginalFilename(file.getOriginalFilename());
        minioFile.setUploadTime(DateUtils.getNowDate());
        minioFile.setUploadUserId(SecurityUtils.getUsername());
        return minioFile;
    }

    /**
     * 通过文件扩展名获取MIME类型（默认返回application/octet-stream）
     */
    private String getContentTypeByExt(String fileExt) {
        Map<String, String> extToMime = new HashMap<>();
        extToMime.put("jpg", "image/jpeg");
        extToMime.put("png", "image/png");
        extToMime.put("gif", "image/gif");
        extToMime.put("zip", "application/zip");
        extToMime.put("pdf", "application/pdf");
        extToMime.put("doc", "application/msword");
        extToMime.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        return extToMime.getOrDefault(fileExt.toLowerCase(), "application/octet-stream");
    }

    /**
     * 普通上传文件校验（大小 + 扩展名）
     */
    private void validateFile(MultipartFile file) throws FileSizeLimitExceededException, InvalidExtensionException {
        // 大小校验
        if (file.getSize() > DEFAULT_MAX_SIZE) {
            throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
        }
        // 扩展名校验
        String ext = getFileExtension(file.getOriginalFilename());
        if (!Arrays.asList(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION).contains(ext)) {
            throw new InvalidExtensionException(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, ext, file.getOriginalFilename());
        }
    }

    /**
     * 获取文件扩展名（小写）
     */
    private String getFileExtension(String originalFilename) {
        if (originalFilename == null || originalFilename.isEmpty()) {
            throw new RuntimeException("文件名为空");
        }
        int dotIndex = originalFilename.lastIndexOf(".");
        if (dotIndex == -1) {
            throw new RuntimeException("文件名无扩展名：" + originalFilename);
        }
        return originalFilename.substring(dotIndex + 1).toLowerCase();
    }

    /**
     * 确保目录格式以"/"结尾
     */
    private String ensureDirFormat(String directory) {
        if (directory == null || directory.isEmpty()) {
            return "";
        }
        return directory.endsWith("/") ? directory : directory + "/";
    }

    /**
     * 计算文件MD5（支持大文件，流式处理）
     */
    public String calculateMD5(MultipartFile file) {
        try (InputStream in = file.getInputStream()) {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
            BigInteger bigInt = new BigInteger(1, md.digest());
            return String.format("%032x", bigInt); // 补零确保32位
        } catch (Exception e) {
            log.error("计算MD5失败：{}", file.getOriginalFilename(), e);
            throw new RuntimeException("计算文件MD5失败：" + e.getMessage());
        }
    }

    /**
     * 计算字节数组MD5（分片MD5校验用）
     */
    public String calculateMD5(byte[] data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(data);
            BigInteger bigInt = new BigInteger(1, md.digest());
            return String.format("%032x", bigInt);
        } catch (Exception e) {
            log.error("计算字节数组MD5失败", e);
            throw new RuntimeException("计算MD5失败：" + e.getMessage());
        }
    }
}
